🎓 Maestría en Inteligencia Artificial Aplicada¶
🖼️ Visión Computacional para Imágenes y Video¶
👨🏫 Profesores¶
- Profesor Titular: Dr. Gilberto Ochoa Ruiz
- Profesor Asistente: MIP Ma. del Refugio Melendez Alfaro
- Profesor Tutor: M. en C. Jose Angel Martinez Navarro
Laboratorio 7.2: Harris Corner Detection¶
Extracción de Características - Detección de Puntos de Interés¶
📌 Detalles de la Actividad¶
- Código: 7.2 Google Colab
- Título: Harris Corner Detection - Feature Detection
- Fecha de entrega: 📅 Octubre 26, 2025 a las 23:59
- Formato de entrega: Notebook (.ipynb) + Informe
- Modalidad: Equipo
👥 Team 13¶
🚀 Nuestro Equipo¶
Javier Augusto Rebull Saucedo¶
Matrícula: |
Juan Carlos Pérez Nava¶
Matrícula: |
Luis Gerardo Sánchez Salazar¶
Matrícula: |
Oscar Enrique García García¶
Matrícula: |
Objetivo del Proyecto¶
El detector de Harris es un algoritmo fundamental en visión computacional para la detección de características locales (feature detection), siendo el primer paso esencial en el pipeline de:
- Extracción de características (Feature Detection & Description)
- Correspondencia entre imágenes (Image Matching)
- Reconstrucción 3D (Structure from Motion)
- Seguimiento de objetos (Object Tracking)
- Calibración de cámaras (Camera Calibration)
- Reconocimiento visual (Visual Recognition)
El método de Harris (1988) detecta puntos de interés (keypoints) que son invariantes a rotación y traslación, siendo la base para algoritmos modernos como SIFT, SURF y ORB.
En este laboratorio implementaremos el detector de Harris desde cero usando Python y NumPy, analizando:
- El comportamiento del algoritmo bajo diferentes condiciones (iluminación, ángulo, escala)
- El sistema de umbrales adaptativos para detección robusta
- Las fortalezas y limitaciones del método Harris
- Comparación visual entre diferentes configuraciones de parámetros
📚 Tabla de Contenidos¶
- Configuración e Instalación
- Descarga y Preparación del Dataset
- Implementación del Algoritmo Harris
- 3.1 Conversión a Escala de Grises
- 3.2 Cálculo de Derivadas Espaciales (Gradientes)
- 3.3 Configuración del Tensor de Estructura
- 3.4 Cálculo de la Respuesta Harris
- 3.5 Detección de Esquinas y Bordes
- Análisis por Objeto
- 4.1 Totoro 🐱
- 4.2 Borrego Tec 🐏
- 4.3 Trumpy 🎺
- 4.4 Amlito 🌮
- 4.5 Jack Daniels 🥃
- 4.6 Simbita 🦁
- 4.7 Cabina London ☎️
- 4.8 All Friends 👥
- Tablas Comparativas y Visualizaciones
- 5.1 Tabla Consolidada de Resultados
- 5.2 Gráficos de Caja (Box Plots)
- 5.3 Análisis Estadístico
- Conclusiones y Análisis de Resultados
- 6.1 Efecto de Iluminación
- 6.2 Efecto de Ángulo de Captura
- 6.3 Efecto de Escala (Zoom)
- 6.4 Fortalezas y Limitaciones
- 6.5 Comparación con Otros Métodos
- Reflexión Final de Equipo
- Referencias (APA 7)
🔑 Conceptos Clave¶
| Concepto | Descripción |
|---|---|
| Corner (Esquina) | Punto donde la intensidad cambia significativamente en múltiples direcciones |
| Edge (Borde) | Punto donde la intensidad cambia en una sola dirección |
| Respuesta Harris (R) | $R = \det(M) - k \cdot \text{trace}(M)^2$ |
| Tensor de Estructura (M) | Matriz que captura gradientes locales en ventana gaussiana |
| Sistema Híbrido | Combina criterio estadístico + percentil para umbrales adaptativos |
| Invarianza | Harris es invariante a rotación/traslación, NO a escala |
📊 Dataset¶
- Total de imágenes: 48 imágenes + 1 demo (chessboard)
- Objetos: 8 diferentes (Totoro, Borrego Tec, Trumpy, Amlito, Jack Daniels, Simbita, Cabina London, All Friends)
- Condiciones por objeto: 6 variaciones
- 📸 Vista frontal normal
- 🌙 Vista frontal oscura
- ➡️ Lateral derecho
- ⬅️ Lateral izquierdo
- 🔍 Zoom (acercamiento)
- 🔄 Otras perspectivas
- Resolución: 800×600 píxeles (480,000 píxeles totales)
🛠️ Tecnologías Utilizadas¶
Última actualización: Octubre 2025
Versión del notebook: 1.0
🎯 Laboratorio 7.2: Harris Corner Detection¶
Extracción de Características - Detección de Puntos de Interés¶
📋 Contexto del Problema¶
En visión computacional, uno de los desafíos fundamentales es encontrar correspondencias entre imágenes que muestran el mismo objeto o escena bajo diferentes condiciones. Este problema es crucial porque las imágenes pueden sufrir múltiples transformaciones:
- Geométricas: Cambios de perspectiva, rotación, escala, traslación
- Fotométricas: Variaciones de iluminación, contraste, exposición
🎯 Pipeline de Extracción de Características¶
El proceso general para encontrar correspondencias se divide en tres etapas:
- Detección (Detection): Identificar puntos de interés (keypoints) distintivos
- Descripción (Description): Calcular un descriptor local para cada keypoint
- Correspondencia (Matching): Comparar descriptores entre imágenes
En este laboratorio nos enfocaremos en la primera etapa: Detección, utilizando el algoritmo de Harris Corner Detector.
🔍 ¿Qué es el Detector de Harris?¶
El Harris Corner Detector (desarrollado por Chris Harris y Mike Stephens en 1988) es un operador matemático que detecta "esquinas" (corners) en una imagen. Una esquina se define como una región donde la intensidad de la imagen cambia significativamente en todas las direcciones.
Distinción de Regiones:¶
- Región plana: No hay cambio de intensidad en ninguna dirección
- Borde (Edge): Cambio de intensidad en una sola dirección
- Esquina (Corner): Cambio de intensidad en múltiples direcciones ✅
Propiedades del Detector de Harris:¶
- ✅ Covariante a traslación y rotación
- ❌ NO invariante a cambios de escala (limitación principal)
- ✅ Robusto a cambios moderados de iluminación
🎓 Objetivos del Laboratorio¶
- Implementar el algoritmo de Harris Corner Detection desde cero
- Analizar el comportamiento del detector bajo diferentes condiciones:
- Diferentes ángulos de captura
- Variaciones de iluminación (claro vs oscuro)
- Cambios de escala (zoom)
- Diferentes perspectivas
- Comparar resultados con diferentes configuraciones de parámetros
- Evaluar las fortalezas y debilidades del método Harris
📚 Referencias¶
- Harris, C., & Stephens, M. (1988). "A Combined Corner and Edge Detector"
- OpenCV Feature Detection Tutorial: https://docs.opencv.org/3.4/db/d27/tutorial_py_table_of_contents_feature2d.html
- Dr. Gilberto Ochoa Ruiz - Módulo 3.2: Extracción de Descriptores
📑 Tabla de Contenidos¶
- Configuración e Instalación
- Descarga de Dataset
- Implementación del Algoritmo Harris
- 3.1 Conversión a Escala de Grises
- 3.2 Cálculo de Derivadas Espaciales (Gradientes)
- 3.3 Configuración del Tensor de Estructura
- 3.4 Cálculo de la Respuesta Harris
- 3.5 Detección de Esquinas y Bordes
- Análisis por Objeto
- Tablas Comparativas
- Conclusiones
1. 🔧 Configuración e Instalación ¶
Importamos las bibliotecas necesarias para el procesamiento de imágenes y el análisis numérico.
# Instalación de dependencias
!pip install gdown -q
# Librerías estándar
import os
import gdown
import pandas as pd
import numpy as np
from pathlib import Path
import shutil
from collections import defaultdict
# Procesamiento de imágenes
import cv2
from scipy import signal as sig
from scipy.ndimage.filters import convolve
from PIL import Image
# Visualización
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.gridspec import GridSpec
import seaborn as sns
# Configuración de visualización
plt.rcParams['figure.figsize'] = (15, 8)
plt.rcParams['font.size'] = 10
sns.set_style("whitegrid")
print("✅ Librerías importadas correctamente")
print(f"📦 OpenCV version: {cv2.__version__}")
print(f"📦 NumPy version: {np.__version__}")
/tmp/ipython-input-2298597382.py:16: DeprecationWarning: Please import `convolve` from the `scipy.ndimage` namespace; the `scipy.ndimage.filters` namespace is deprecated and will be removed in SciPy 2.0.0. from scipy.ndimage.filters import convolve
✅ Librerías importadas correctamente 📦 OpenCV version: 4.12.0 📦 NumPy version: 2.0.2
2. 📥 Descarga y Preparación del Dataset ¶
Descargamos las imágenes desde Google Drive utilizando gdown. El dataset incluye:
- Imagen Demo: Tablero de ajedrez (chessboard) - ideal para detectar esquinas
- 8 Objetos diferentes capturados bajo múltiples condiciones:
- Iluminación: Normal y oscura
- Ángulos: Frontal, lateral izquierdo, lateral derecho
- Escala: Vista normal y zoom
- Otras perspectivas
🎯 Objetos del Dataset:¶
- Totoro 🐱
- Borrego Tec 🐏
- Trumpy 🎺
- Amlito 🌮
- Jack Daniels 🥃
- Simbita 🦁
- Cabina London ☎️
- All Friends 👥
# Crear estructura de directorios
data_dir = Path('data')
data_dir.mkdir(exist_ok=True)
# Directorios por objeto
objects = ['01_Totoro', '02_BorregoTec', '03_Trumpy', '04_Amlito',
'05_JackDaniels', '06_Simbita', '07_CabinaLondon', '08_AllFriends']
for obj in objects:
(data_dir / obj).mkdir(exist_ok=True)
print("✅ Estructura de directorios creada")
✅ Estructura de directorios creada
# Datos del CSV (imágenes y sus IDs de Google Drive)
image_data = {
'nombre_archivo': [
'7.2_Characteristics_Extraction_chessboard.jpg',
'01_Totoro_01_Totoro_Front.jpg',
'01_Totoro_02_Totoro_Front_Dark.jpg',
'01_Totoro_03_Totoro_Lateral_Derecho.jpg',
'01_Totoro_04_Totoro_Lateral_Izquierdo.jpg',
'01_Totoro_05_Totoro_Zoom.jpg',
'01_Totoro_06_Totoro_Other.jpg',
'02_BorregoTec_01_BorregoTec_Front.jpg',
'02_BorregoTec_02_BorregoTec_dark.jpg',
'02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg',
'02_BorregoTec_04_BorregoTec_Lateral_Izquierdo.jpg',
'02_BorregoTec_05_BorregoTec_Zoom.jpg',
'02_BorregoTec_06_BorregoTec_Lateral_Other.jpg',
'03_Trumpy_01_Trumpy_Front.jpg',
'03_Trumpy_02_Trumpy_Front_Dark.jpg',
'03_Trumpy_03_Trumpy_Latera_Derecho.jpg',
'03_Trumpy_03_Trumpy_Latera_Izquierdo.jpg',
'03_Trumpy_05_Trumpy_Zoom.jpg',
'03_Trumpy_06_Trumpy_Latera_Other.jpg',
'04_Amlito_01_Amlito_Front.jpg',
'04_Amlito_02_Amlito_Front_Dark.jpg',
'04_Amlito_03_Amlito_Lateral_Derecho.jpg',
'04_Amlito_04_Amlito_Lateral_Izquierdo.jpg',
'04_Amlito_05_Amlito_Zoom.jpg',
'04_Amlito_06_Amlito_Other.jpg',
'05_JackDaniels_01_JackDaniel_Front.jpg',
'05_JackDaniels_02_JackDaniel_Front_Dark.jpg',
'05_JackDaniels_03_JackDaniel_Lateral_Derecho.jpg',
'05_JackDaniels_04_JackDaniel_Lateral_Izquierdo.jpg',
'05_JackDaniels_05_JackDaniel_Zoom.jpg',
'05_JackDaniels_06_JackDaniel_Zoom.jpg',
'06_Simbita_01_Simbita_Front.jpg',
'06_Simbita_02_Simbita_Front_Dark.jpg',
'06_Simbita_03_Simbita_Lateral_Derecho.jpg',
'06_Simbita_04_Simbita_Lateral_Izquierdo.jpg',
'06_Simbita_05_Simbita_Lateral_Zoom.jpg',
'06_Simbita_06_Simbita_Other.jpg',
'07_CabinaLondon_01_LondonPhone_Front.jpg',
'07_CabinaLondon_02_LondonPhone_Front_Dark.jpg',
'07_CabinaLondon_03_LondonPhone_Lateral_Derecho.jpg',
'07_CabinaLondon_04_LondonPhone_Lateral_Izquierdo.jpg',
'07_CabinaLondon_05_LondonPhone_Lateral_Zoom.jpg',
'07_CabinaLondon_06_LondonPhone_Other.jpg',
'08_AllFriends_01_AllFriends.jpg',
'08_AllFriends_02_AllFriends.jpg',
'08_AllFriends_03_AllFriends.jpg',
'08_AllFriends_04_AllFriends.jpg',
'08_AllFriends_05_AllFriends.jpg',
'08_AllFriends_06_AllFriends.jpg'
],
'id_archivo': [
'1BF_FznGku-lPAvXmSEJYoAfyTZeKSOl4',
'17ZsTctI7UnnAX6SjSEEiir8AYr7wwRUu',
'11SrPWXTzGV9FFy3O9rHiX8iC6KxgWUZR',
'1ocgQo98lWu6PlqLODq-NzCRQXROi9h-d',
'1gLHotGjFi3DLZ-udZS-6rOQVBamtcjKK',
'1ZrijkckHyRti0T6ZWi9Sq-wt_si-g-82',
'1dC_kTWr3pUjmvmINatctkH8PQjnAIEby',
'15fTnfAVEbR9wEexW3p_g_NiiGtjhmGGq',
'19AwTTtt_-8dqJjvFlf0xar9jSEWByJA7',
'1RxenQTKirK4_DWZT5toqZ0sClq-yRCza',
'1Oodlm15TdlxgKpVX3iQ5VgBFRUeS5u3j',
'1dsooXHr1I6j1Dodw2O5DLcqU_A34HbUJ',
'1GJjlMxXb7ARJhWWcoXDghHq7SvJ8vFnF',
'1Z0nkRn_R7lC8hsFcwAVQlOEzs2FRvL9-',
'1AEaaWwXnxT-3KmBQP5EQdT5dbzLJdtLh',
'1tWxzx445HkAHDuhoCUVXugwUH6MbQfQr',
'193DkPWUwHcjNBUxirwUDHUwZVkAlJQUe',
'1fxNjAlXG6GYkIA-ACNgfR-WXKEnqkooQ',
'1wUUyi7993Ty9DWlm1THQhtWG0-KjluvN',
'1jG9piTePQ3u3VJ3OcSTdW-OrHmnAwLFx',
'17s9p-tzfUslQ1JihKwcB9NDVYWNUINTe',
'12MinhNNYdJUamHldK1wNLE1csA6wQRrs',
'1wFZXKV5c0RN5yHZ5pZZCiWiwCvFEq5p9',
'1rdTQM3iOGfeQFWez-Jd6GdEDiBcTBj5S',
'1mWJchSLnsm7eYGP7ae-fyGu-VJSxdPP9',
'1SZ7ubx8TeELNX0gWJuwa7CNp4mzr79gC',
'1tpxgqJu2_C0SBgv7K9_Xhx26dx0tnu2F',
'1JYNtgVFYsUI-8zUTy0cS148JSM4aQiss',
'1kJJormn6waOeyR-1ah40tjaJ5mzCY4Iq',
'1-TaQmC2Lrf0LcsOoYkIHxdrWLD6Fq1VD',
'1EiJBK7ELnFFhE4GiJkDZU7VGEqWkIb9E',
'15k0E1w0pcpfjY5Ya-iMlrcEr0olfgf-r',
'13gspVjsuf2dSaYJwNlGrwDsqXBFPjGUH',
'1lptaj05KxHiEnn1enEVoV12_1vIZeQ0k',
'1KKGaF7563g2WMeJGg272CsW56-5zPFIt',
'1-n4SqFhnYg7N0iHbJfBwKTnZjZ3mg2rj',
'1EQpeyyHCWt7L0ZPq7vve9XQ3lsggfQOg',
'1Uq-w02e-fiYrd09JRJ5xVm7ACKELw8ir',
'1BArjDQWFIYkb_qOQh7ErCkvYejnJEOa2',
'1l2jN8xkvF6STAl0m1yxgRA7Vhd5SkW1i',
'1bqNshlrvEv9KXW4nXEFaFj367FQ9ZDW8',
'1yqNGwlBWgYaLMHB5y0QIAaIoerBidorD',
'1Ho4FeVT9srj0UM1mkm7j6-C2pXfjfw_N',
'1Jig8jKnv0WpR_jYCsHozmu9VhgBRCE0h',
'1mEARcofi2vVgTHsYlCz1nCaBWq8iHB3d',
'1yPJkA_c4RF8BuJEglI7vqs_xxLaUsp3l',
'19igYU1t3zhI79PXTZC7VM7FdhkUNhpvV',
'1NfxzEMd6I1GjbhXeE0ANp9HB5DULQDcF',
'13F54duRAXmYEUsBaeZf-4LaH3GslFShA'
]
}
df_images = pd.DataFrame(image_data)
print(f"✅ Dataset: {len(df_images)} imágenes registradas")
df_images.head(10)
✅ Dataset: 49 imágenes registradas
| nombre_archivo | id_archivo | |
|---|---|---|
| 0 | 7.2_Characteristics_Extraction_chessboard.jpg | 1BF_FznGku-lPAvXmSEJYoAfyTZeKSOl4 |
| 1 | 01_Totoro_01_Totoro_Front.jpg | 17ZsTctI7UnnAX6SjSEEiir8AYr7wwRUu |
| 2 | 01_Totoro_02_Totoro_Front_Dark.jpg | 11SrPWXTzGV9FFy3O9rHiX8iC6KxgWUZR |
| 3 | 01_Totoro_03_Totoro_Lateral_Derecho.jpg | 1ocgQo98lWu6PlqLODq-NzCRQXROi9h-d |
| 4 | 01_Totoro_04_Totoro_Lateral_Izquierdo.jpg | 1gLHotGjFi3DLZ-udZS-6rOQVBamtcjKK |
| 5 | 01_Totoro_05_Totoro_Zoom.jpg | 1ZrijkckHyRti0T6ZWi9Sq-wt_si-g-82 |
| 6 | 01_Totoro_06_Totoro_Other.jpg | 1dC_kTWr3pUjmvmINatctkH8PQjnAIEby |
| 7 | 02_BorregoTec_01_BorregoTec_Front.jpg | 15fTnfAVEbR9wEexW3p_g_NiiGtjhmGGq |
| 8 | 02_BorregoTec_02_BorregoTec_dark.jpg | 19AwTTtt_-8dqJjvFlf0xar9jSEWByJA7 |
| 9 | 02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg | 1RxenQTKirK4_DWZT5toqZ0sClq-yRCza |
def download_and_resize_image(file_id, output_path, max_size=800):
"""
Descarga una imagen desde Google Drive y la redimensiona para optimizar espacio.
Args:
file_id: ID del archivo en Google Drive
output_path: Ruta donde guardar la imagen
max_size: Tamaño máximo en píxeles (se mantiene aspect ratio)
"""
try:
# Descargar imagen temporal
temp_path = 'temp_image.jpg'
url = f'https://drive.google.com/uc?id={file_id}'
gdown.download(url, temp_path, quiet=True)
# Cargar y redimensionar
img = Image.open(temp_path)
# Calcular nuevo tamaño manteniendo aspect ratio
width, height = img.size
if max(width, height) > max_size:
if width > height:
new_width = max_size
new_height = int(height * (max_size / width))
else:
new_height = max_size
new_width = int(width * (max_size / height))
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Guardar optimizada
img.save(output_path, 'JPEG', quality=85, optimize=True)
# Eliminar temporal
os.remove(temp_path)
return True
except Exception as e:
print(f"❌ Error descargando {output_path}: {e}")
return False
# Descargar todas las imágenes
print("📥 Descargando imágenes...\n")
downloaded = 0
for idx, row in df_images.iterrows():
filename = row['nombre_archivo']
file_id = row['id_archivo']
# Determinar carpeta de destino
if 'chessboard' in filename:
output_path = data_dir / filename
else:
# Extraer prefijo del objeto
prefix = filename.split('_')[0] + '_' + filename.split('_')[1]
output_path = data_dir / prefix / filename
if download_and_resize_image(file_id, str(output_path)):
downloaded += 1
if (downloaded % 5 == 0):
print(f"✓ Descargadas: {downloaded}/{len(df_images)}")
print(f"\n✅ Descarga completada: {downloaded}/{len(df_images)} imágenes")
print(f"📁 Imágenes guardadas en: {data_dir.absolute()}")
📥 Descargando imágenes... ✓ Descargadas: 5/49 ✓ Descargadas: 10/49 ✓ Descargadas: 15/49 ✓ Descargadas: 20/49 ✓ Descargadas: 25/49 ✓ Descargadas: 30/49 ✓ Descargadas: 35/49 ✓ Descargadas: 40/49 ✓ Descargadas: 45/49 ✅ Descarga completada: 49/49 imágenes 📁 Imágenes guardadas en: /content/data
3. 🔬 Implementación del Algoritmo Harris ¶
El algoritmo de Harris se basa en el análisis de la matriz de estructura (también llamada matriz de segundo momento) de una imagen. Esta matriz captura la variación de intensidad en diferentes direcciones alrededor de cada píxel.
📐 Fundamento Matemático¶
Para cada píxel, se construye una matriz de estructura $M$:
$$M = \begin{bmatrix} I_x^2 & I_x I_y \\ I_x I_y & I_y^2 \end{bmatrix}$$
Donde:
- $I_x$ = Derivada de la imagen en dirección x (gradiente horizontal)
- $I_y$ = Derivada de la imagen en dirección y (gradiente vertical)
La función de respuesta Harris se calcula como:
$$R = det(M) - k \cdot trace(M)^2$$
Donde:
- $det(M) = I_x^2 \cdot I_y^2 - (I_x I_y)^2$ (determinante)
- $trace(M) = I_x^2 + I_y^2$ (traza)
- $k$ = constante empírica (típicamente 0.04 - 0.06)
Interpretación de R:¶
- R > 0 → Esquina (corner) ✅
- R < 0 → Borde (edge)
- R ≈ 0 → Región plana
Ahora implementaremos cada paso del algoritmo de forma modular.
3.1 📸 Conversión a Escala de Grises ¶
El primer paso es convertir la imagen a escala de grises. Esto simplifica el procesamiento al reducir la imagen a un solo canal de intensidad.
¿Por qué escala de grises?
- Reduce la complejidad computacional (de 3 canales a 1)
- Los gradientes de intensidad son suficientes para detectar esquinas
- El algoritmo de Harris trabaja con derivadas de intensidad
Utilizaremos la imagen de chessboard (tablero de ajedrez) como demostración, ya que contiene esquinas bien definidas ideales para el detector de Harris.
# Cargar imagen demo (chessboard)
chessboard_path = str(data_dir / '7.2_Characteristics_Extraction_chessboard.jpg')
img = cv2.imread(chessboard_path)
img_color = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Visualización
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
axes[0].imshow(img_color)
axes[0].set_title('Imagen Original (RGB)', fontsize=14, fontweight='bold')
axes[0].axis('off')
axes[1].imshow(img_gray, cmap='gray')
axes[1].set_title('Imagen en Escala de Grises', fontsize=14, fontweight='bold')
axes[1].axis('off')
plt.tight_layout()
plt.show()
print(f"📏 Dimensiones de la imagen: {img_gray.shape}")
print(f"📊 Rango de valores: [{img_gray.min()}, {img_gray.max()}]")
📏 Dimensiones de la imagen: (612, 612) 📊 Rango de valores: [0, 255]
3.2 📊 Cálculo de Derivadas Espaciales (Gradientes) ¶
Las derivadas espaciales (gradientes) nos indican dónde y en qué dirección cambia la intensidad de la imagen.
Operador Sobel¶
Utilizamos el operador Sobel, que combina suavizado Gaussiano con derivación:
Kernel para gradiente en X (horizontal): $$S_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}$$
Kernel para gradiente en Y (vertical): $$S_y = \begin{bmatrix} 1 & 2 & 1 \\ 0 & 0 & 0 \\ -1 & -2 & -1 \end{bmatrix}$$
Estos kernels detectan cambios de intensidad:
- $I_x$: Bordes verticales (cambios horizontales)
- $I_y$: Bordes horizontales (cambios verticales)
def gradient_x(img_gray):
"""
Calcula el gradiente en dirección X usando el operador Sobel.
Detecta bordes verticales (cambios de intensidad horizontal).
"""
kernel_x = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
return sig.convolve2d(img_gray, kernel_x, mode='same')
def gradient_y(img_gray):
"""
Calcula el gradiente en dirección Y usando el operador Sobel.
Detecta bordes horizontales (cambios de intensidad vertical).
"""
kernel_y = np.array([[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]])
return sig.convolve2d(img_gray, kernel_y, mode='same')
# Calcular gradientes
I_x = gradient_x(img_gray)
I_y = gradient_y(img_gray)
# Calcular magnitud del gradiente para visualización
gradient_magnitude = np.sqrt(I_x**2 + I_y**2)
# Visualización
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
axes[0, 0].imshow(img_gray, cmap='gray')
axes[0, 0].set_title('Imagen Original (Grayscale)', fontsize=12, fontweight='bold')
axes[0, 0].axis('off')
im1 = axes[0, 1].imshow(I_x, cmap='seismic')
axes[0, 1].set_title('Gradiente X (Bordes Verticales)', fontsize=12, fontweight='bold')
axes[0, 1].axis('off')
plt.colorbar(im1, ax=axes[0, 1], fraction=0.046)
im2 = axes[1, 0].imshow(I_y, cmap='seismic')
axes[1, 0].set_title('Gradiente Y (Bordes Horizontales)', fontsize=12, fontweight='bold')
axes[1, 0].axis('off')
plt.colorbar(im2, ax=axes[1, 0], fraction=0.046)
im3 = axes[1, 1].imshow(gradient_magnitude, cmap='hot')
axes[1, 1].set_title('Magnitud del Gradiente', fontsize=12, fontweight='bold')
axes[1, 1].axis('off')
plt.colorbar(im3, ax=axes[1, 1], fraction=0.046)
plt.tight_layout()
plt.show()
print(f"📊 Estadísticas del Gradiente X:")
print(f" Min: {I_x.min():.2f}, Max: {I_x.max():.2f}, Mean: {I_x.mean():.2f}")
print(f"\n📊 Estadísticas del Gradiente Y:")
print(f" Min: {I_y.min():.2f}, Max: {I_y.max():.2f}, Mean: {I_y.mean():.2f}")
📊 Estadísticas del Gradiente X: Min: -1020.00, Max: 1020.00, Mean: -0.83 📊 Estadísticas del Gradiente Y: Min: -1020.00, Max: 1020.00, Mean: 0.82
3.3 🔢 Configuración del Tensor de Estructura ¶
El tensor de estructura (o matriz de segundo momento) captura la distribución de gradientes alrededor de cada píxel. Para construirlo, necesitamos calcular los productos de los gradientes y aplicar un suavizado Gaussiano.
Componentes de la Matriz de Estructura:¶
$$M = \begin{bmatrix} I_{xx} & I_{xy} \\ I_{xy} & I_{yy} \end{bmatrix}$$
Donde:
- $I_{xx} = G * (I_x^2)$ → Varianza en dirección X
- $I_{yy} = G * (I_y^2)$ → Varianza en dirección Y
- $I_{xy} = G * (I_x \cdot I_y)$ → Covarianza entre X y Y
$G$ representa una convolución con un filtro Gaussiano que suaviza las mediciones locales.
¿Por qué suavizado Gaussiano?¶
- Reduce el ruido en las mediciones de gradiente
- Define el "vecindario" considerado alrededor de cada píxel
- $\sigma$ (sigma) controla el tamaño de la ventana de análisis
def gaussian_kernel(size, sigma=1):
"""
Genera un kernel Gaussiano 2D para suavizado.
Args:
size: Tamaño del kernel (debe ser impar)
sigma: Desviación estándar de la Gaussiana
Returns:
Kernel Gaussiano normalizado
"""
size = int(size) // 2
x, y = np.mgrid[-size:size+1, -size:size+1]
normal = 1 / (2.0 * np.pi * sigma**2)
g = np.exp(-((x**2 + y**2) / (2.0*sigma**2))) * normal
return g
# Visualizar el kernel Gaussiano
kernel_3x3 = gaussian_kernel(3, sigma=1)
kernel_5x5 = gaussian_kernel(5, sigma=1.5)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Kernel 3x3
im1 = axes[0].imshow(kernel_3x3, cmap='viridis')
axes[0].set_title('Kernel Gaussiano 3x3 (σ=1)', fontsize=12, fontweight='bold')
plt.colorbar(im1, ax=axes[0])
# Kernel 5x5
im2 = axes[1].imshow(kernel_5x5, cmap='viridis')
axes[1].set_title('Kernel Gaussiano 5x5 (σ=1.5)', fontsize=12, fontweight='bold')
plt.colorbar(im2, ax=axes[1])
# Perfil 1D
axes[2].plot(kernel_3x3[1, :], 'o-', label='3x3 (σ=1)', linewidth=2)
axes[2].plot(kernel_5x5[2, :], 's-', label='5x5 (σ=1.5)', linewidth=2)
axes[2].set_title('Perfil 1D del Kernel', fontsize=12, fontweight='bold')
axes[2].set_xlabel('Posición')
axes[2].set_ylabel('Peso')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("🔬 Kernel Gaussiano 3x3:")
print(kernel_3x3)
print(f"\n✓ Suma normalizada: {kernel_3x3.sum():.6f} (debe ser ≈ 1.0)")
🔬 Kernel Gaussiano 3x3: [[0.05854983 0.09653235 0.05854983] [0.09653235 0.15915494 0.09653235] [0.05854983 0.09653235 0.05854983]] ✓ Suma normalizada: 0.779484 (debe ser ≈ 1.0)
# Calcular los componentes del tensor de estructura
# Aplicamos suavizado Gaussiano a los productos de gradientes
Ixx = convolve(I_x**2, gaussian_kernel(3, 1))
Ixy = convolve(I_y * I_x, gaussian_kernel(3, 1))
Iyy = convolve(I_y**2, gaussian_kernel(3, 1))
# Visualización de los componentes del tensor
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
im1 = axes[0].imshow(Ixx, cmap='hot')
axes[0].set_title('$I_{xx}$ = G * ($I_x^2$)\nVarianza en X', fontsize=12, fontweight='bold')
axes[0].axis('off')
plt.colorbar(im1, ax=axes[0], fraction=0.046)
im2 = axes[1].imshow(Ixy, cmap='seismic')
axes[1].set_title('$I_{xy}$ = G * ($I_x \cdot I_y$)\nCovarianza X-Y', fontsize=12, fontweight='bold')
axes[1].axis('off')
plt.colorbar(im2, ax=axes[1], fraction=0.046)
im3 = axes[2].imshow(Iyy, cmap='hot')
axes[2].set_title('$I_{yy}$ = G * ($I_y^2$)\nVarianza en Y', fontsize=12, fontweight='bold')
axes[2].axis('off')
plt.colorbar(im3, ax=axes[2], fraction=0.046)
plt.tight_layout()
plt.show()
print("📊 Estadísticas del Tensor de Estructura:")
print(f"\nIxx (Varianza X):")
print(f" Min: {Ixx.min():.2f}, Max: {Ixx.max():.2f}, Mean: {Ixx.mean():.2f}")
print(f"\nIxy (Covarianza):")
print(f" Min: {Ixy.min():.2f}, Max: {Ixy.max():.2f}, Mean: {Ixy.mean():.2f}")
print(f"\nIyy (Varianza Y):")
print(f" Min: {Iyy.min():.2f}, Max: {Iyy.max():.2f}, Mean: {Iyy.mean():.2f}")
<>:17: SyntaxWarning: invalid escape sequence '\c'
<>:17: SyntaxWarning: invalid escape sequence '\c'
/tmp/ipython-input-3714679759.py:17: SyntaxWarning: invalid escape sequence '\c'
axes[1].set_title('$I_{xy}$ = G * ($I_x \cdot I_y$)\nCovarianza X-Y', fontsize=12, fontweight='bold')
📊 Estadísticas del Tensor de Estructura: Ixx (Varianza X): Min: 0.00, Max: 588712.00, Mean: 21036.45 Ixy (Covarianza): Min: -233406.00, Max: 240392.00, Mean: 5.73 Iyy (Varianza Y): Min: 0.00, Max: 588725.00, Mean: 21000.93
3.4 🎯 Cálculo de la Respuesta Harris ¶
La función de respuesta Harris $R$ combina el determinante y la traza de la matriz de estructura para clasificar cada píxel:
$$R = det(M) - k \cdot trace(M)^2$$
Donde:
- $det(M) = I_{xx} \cdot I_{yy} - I_{xy}^2$ (mide la "fuerza" de la esquina)
- $trace(M) = I_{xx} + I_{yy}$ (suma de varianzas)
- $k$ = 0.04 - 0.06 (constante empírica de Harris)
📊 Interpretación del Valor R:¶
| Valor de R | Interpretación | Características |
|---|---|---|
| R >> 0 (alto positivo) | Esquina | Cambio de intensidad en todas direcciones |
| R << 0 (negativo) | Borde | Cambio en una sola dirección |
| R ≈ 0 | Región plana | Sin cambios significativos |
🔍 Efecto del parámetro k:¶
- k más pequeño (0.04): Más esquinas detectadas (más sensible)
- k más grande (0.06): Menos esquinas detectadas (más selectivo)
# Parámetro k de Harris (valor típico: 0.04 - 0.06)
k = 0.05
# Calcular determinante y traza
detA = Ixx * Iyy - Ixy ** 2 # Determinante de la matriz M
traceA = Ixx + Iyy # Traza de la matriz M
# Calcular respuesta Harris
harris_response = detA - k * traceA ** 2
# Normalizar para visualización
harris_normalized = cv2.normalize(harris_response, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
# Visualización de componentes y resultado
fig = plt.figure(figsize=(18, 12))
gs = GridSpec(2, 3, figure=fig)
# Fila 1: Componentes
ax1 = fig.add_subplot(gs[0, 0])
im1 = ax1.imshow(detA, cmap='hot')
ax1.set_title(f'Determinante\ndet(M) = $I_{{xx}} \cdot I_{{yy}} - I_{{xy}}^2$',
fontsize=11, fontweight='bold')
ax1.axis('off')
plt.colorbar(im1, ax=ax1, fraction=0.046)
ax2 = fig.add_subplot(gs[0, 1])
im2 = ax2.imshow(traceA, cmap='viridis')
ax2.set_title(f'Traza\ntrace(M) = $I_{{xx}} + I_{{yy}}$',
fontsize=11, fontweight='bold')
ax2.axis('off')
plt.colorbar(im2, ax=ax2, fraction=0.046)
ax3 = fig.add_subplot(gs[0, 2])
im3 = ax3.imshow(harris_response, cmap='seismic')
ax3.set_title(f'Respuesta Harris (k={k})\nR = det(M) - k·trace(M)²',
fontsize=11, fontweight='bold')
ax3.axis('off')
plt.colorbar(im3, ax=ax3, fraction=0.046)
# Fila 2: Análisis
ax4 = fig.add_subplot(gs[1, :])
ax4.hist(harris_response.flatten(), bins=100, color='steelblue', alpha=0.7, edgecolor='black')
ax4.axvline(0, color='red', linestyle='--', linewidth=2, label='R = 0 (transición)')
ax4.set_xlabel('Valor de R', fontsize=12)
ax4.set_ylabel('Frecuencia', fontsize=12)
ax4.set_title('Distribución de Valores de Respuesta Harris', fontsize=13, fontweight='bold')
ax4.legend(fontsize=10)
ax4.grid(True, alpha=0.3)
# Añadir anotaciones de regiones
y_max = ax4.get_ylim()[1]
ax4.text(-np.abs(harris_response.max())*0.7, y_max*0.9, 'Bordes\n(R < 0)',
fontsize=11, ha='center', bbox=dict(boxstyle='round', facecolor='lightcoral', alpha=0.7))
ax4.text(0, y_max*0.9, 'Plano\n(R ≈ 0)',
fontsize=11, ha='center', bbox=dict(boxstyle='round', facecolor='lightgray', alpha=0.7))
ax4.text(harris_response.max()*0.7, y_max*0.9, 'Esquinas\n(R > 0)',
fontsize=11, ha='center', bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))
plt.tight_layout()
plt.show()
# Estadísticas
print("📊 Estadísticas de la Respuesta Harris:")
print(f"\n Valor mínimo (bordes más fuertes): {harris_response.min():.2e}")
print(f" Valor máximo (esquinas más fuertes): {harris_response.max():.2e}")
print(f" Valor medio: {harris_response.mean():.2e}")
print(f" Desviación estándar: {harris_response.std():.2e}")
# Conteo de píxeles por categoría
corners = np.sum(harris_response > 0)
edges = np.sum(harris_response < 0)
flat = np.sum(harris_response == 0)
total = harris_response.size
print(f"\n📍 Distribución de píxeles:")
print(f" Esquinas (R > 0): {corners:,} ({100*corners/total:.2f}%)")
print(f" Bordes (R < 0): {edges:,} ({100*edges/total:.2f}%)")
print(f" Planos (R = 0): {flat:,} ({100*flat/total:.2f}%)")
<>:21: SyntaxWarning: invalid escape sequence '\c'
<>:21: SyntaxWarning: invalid escape sequence '\c'
/tmp/ipython-input-2119090743.py:21: SyntaxWarning: invalid escape sequence '\c'
ax1.set_title(f'Determinante\ndet(M) = $I_{{xx}} \cdot I_{{yy}} - I_{{xy}}^2$',
📊 Estadísticas de la Respuesta Harris: Valor mínimo (bordes más fuertes): -1.73e+10 Valor máximo (esquinas más fuertes): 7.13e+10 Valor medio: -9.14e+08 Desviación estándar: 4.04e+09 📍 Distribución de píxeles: Esquinas (R > 0): 3,472 (0.93%) Bordes (R < 0): 73,899 (19.73%) Planos (R = 0): 297,173 (79.34%)
3.5 🎨 Detección de Esquinas y Bordes ¶
Ahora utilizamos el valor de $R$ para clasificar y visualizar esquinas y bordes en la imagen.
🎯 Criterios de Detección:¶
Básico:
- R > 0 → Marcar como esquina (rojo)
- R < 0 → Marcar como borde (verde)
Mejorado (con umbrales):
- R > percentil 99 → Esquinas fuertes (solo el 1% superior)
- R < percentil 5 → Bordes fuertes (solo el 5% inferior)
🔧 Mejoras Adicionales:¶
- Gaussian Blur: Reduce ruido antes del procesamiento
- Umbrales adaptativos: Usa percentiles en lugar de valores fijos
- Supresión de no-máximos: Elimina detecciones redundantes (opcional)
# Crear copias de la imagen en RGB desde el inicio
img_copy_for_corners = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)
img_copy_for_edges = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)
# SISTEMA HÍBRIDO DE UMBRALES
positive_values = harris_response[harris_response > 0]
negative_values = harris_response[harris_response < 0]
# Calcular umbrales para ESQUINAS
if len(positive_values) > 0:
mean_pos = positive_values.mean()
std_pos = positive_values.std()
corner_threshold_stat = mean_pos + 1.2 * std_pos
corner_threshold_perc = np.percentile(harris_response, 96)
corner_threshold = min(corner_threshold_stat, corner_threshold_perc) # El más permisivo
print(f"🔴 Umbral esquinas (estadístico): {corner_threshold_stat:.2e}")
print(f"🔴 Umbral esquinas (percentil 98): {corner_threshold_perc:.2e}")
print(f"🔴 Umbral esquinas (seleccionado): {corner_threshold:.2e} ← min(ambos)")
else:
corner_threshold = np.percentile(harris_response, 98)
# Calcular umbrales para BORDES
if len(negative_values) > 0:
mean_neg = negative_values.mean()
std_neg = negative_values.std()
edge_threshold_stat = mean_neg - 2.5 * std_neg
edge_threshold_perc = np.percentile(harris_response, 0.2)
edge_threshold = max(edge_threshold_stat, edge_threshold_perc) # El más permisivo
print(f"\n🟢 Umbral bordes (estadístico): {edge_threshold_stat:.2e}")
print(f"🟢 Umbral bordes (percentil 2): {edge_threshold_perc:.2e}")
print(f"🟢 Umbral bordes (seleccionado): {edge_threshold:.2e} ← max(ambos)")
else:
edge_threshold = np.percentile(harris_response, 0.2)
# Parámetros de visualización
corner_radius = 6
edge_radius = 5
# Detección con CÍRCULOS SÚPER MARCADOS
corner_count = 0
edge_count = 0
for rowindex, response in enumerate(harris_response):
for colindex, r in enumerate(response):
if r > corner_threshold:
# TRIPLE CAPA ROJA
cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius+2, (255, 100, 100), 1)
cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (255, 0, 0), -1)
cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (150, 0, 0), 2)
corner_count += 1
elif r < edge_threshold:
# TRIPLE CAPA VERDE
cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius+2, (100, 255, 100), 1)
cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 255, 0), -1)
cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 150, 0), 2)
edge_count += 1
corners_rgb = img_copy_for_corners
edges_rgb = img_copy_for_edges
# Visualización
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
axes[0].imshow(img_color)
axes[0].set_title('Imagen Original', fontsize=13, fontweight='bold')
axes[0].axis('off')
axes[1].imshow(corners_rgb)
axes[1].set_title(f'Esquinas Detectadas\n{corner_count:,} esquinas (radio {corner_radius}px)',
fontsize=13, fontweight='bold', color='darkred')
axes[1].axis('off')
axes[2].imshow(edges_rgb)
axes[2].set_title(f'Bordes Detectados\n{edge_count:,} bordes (radio {edge_radius}px)',
fontsize=13, fontweight='bold', color='darkgreen')
axes[2].axis('off')
plt.tight_layout()
plt.show()
print(f"\n✅ Esquinas detectadas: {corner_count:,}")
print(f"✅ Bordes detectados: {edge_count:,}")
print(f"\n💡 Sistema híbrido garantiza variabilidad entre imágenes")
🔴 Umbral esquinas (estadístico): 2.44e+10 🔴 Umbral esquinas (percentil 98): 0.00e+00 🔴 Umbral esquinas (seleccionado): 0.00e+00 ← min(ambos) 🟢 Umbral bordes (estadístico): -2.27e+10 🟢 Umbral bordes (percentil 2): -1.73e+10 🟢 Umbral bordes (seleccionado): -1.73e+10 ← max(ambos)
✅ Esquinas detectadas: 3,472 ✅ Bordes detectados: 272 💡 Sistema híbrido garantiza variabilidad entre imágenes
🔧 Función Mejorada de Detección Harris¶
Implementamos una función completa que incluye:
- Preprocesamiento opcional (Gaussian Blur)
- Umbrales adaptativos basados en percentiles
- Visualización con mapa de calor
# ========================================
# 🔧 FUNCIÓN HARRIS CORNER DETECTION
# ========================================
def harris_corner_detection(img, img_gray, gaussian_blur=False, improve_thresholds=False, k=0.05,
corner_radius=3, edge_radius=3):
"""
Implementación completa del Detector de Harris con sistema híbrido de umbrales CORREGIDO.
CAMBIOS PRINCIPALES:
- Percentil de esquinas: 98 → 96 (menos agresivo)
- Percentil de bordes: 2 → 4 (menos agresivo)
- Multiplicador sigma: 0.8 → 1.2 (criterio estadístico más permisivo)
Esto hace que el criterio ESTADÍSTICO sea más relevante y genere variabilidad real.
"""
# 1. Preprocesamiento
if gaussian_blur:
img_gray = cv2.GaussianBlur(img_gray, (5, 5), 1.5)
# 2. Gradientes
I_x = gradient_x(img_gray)
I_y = gradient_y(img_gray)
# 3. Tensor de estructura
Ixx = convolve(I_x**2, gaussian_kernel(3, 2))
Ixy = convolve(I_y*I_x, gaussian_kernel(3, 2))
Iyy = convolve(I_y**2, gaussian_kernel(3, 2))
# 4. Respuesta Harris
detA = Ixx * Iyy - Ixy ** 2
traceA = Ixx + Iyy
harris_response = detA - k * traceA ** 2
# 5. Heatmap mejorado
response_clipped = np.clip(harris_response,
np.percentile(harris_response, 1),
np.percentile(harris_response, 99))
harris_norm = cv2.normalize(response_clipped, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
harris_colormap = cv2.applyColorMap(harris_norm, cv2.COLORMAP_HOT)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
harris_colormap_rgb = cv2.cvtColor(harris_colormap, cv2.COLOR_BGR2RGB)
harris_overlay = cv2.addWeighted(img_rgb, 0.4, harris_colormap_rgb, 0.6, 0)
# 6. ⭐ SISTEMA HÍBRIDO DE UMBRALES ⭐
if improve_thresholds:
positive_values = harris_response[harris_response > 0]
negative_values = harris_response[harris_response < 0]
# ESQUINAS: min(estadístico, percentil) = el más permisivo
if len(positive_values) > 0:
mean_pos = positive_values.mean()
std_pos = positive_values.std()
corner_threshold_stat = mean_pos + 0.5 * std_pos
corner_threshold_perc = np.percentile(harris_response, 99.7)
corner_threshold = min(corner_threshold_stat, corner_threshold_perc)
else:
corner_threshold = np.percentile(harris_response, 99.7)
# BORDES: max(estadístico, percentil) = el más permisivo
if len(negative_values) > 0:
mean_neg = negative_values.mean()
std_neg = negative_values.std()
edge_threshold_stat = mean_neg - 0.55 * std_neg
edge_threshold_perc = np.percentile(harris_response, 0.8)
edge_threshold = max(edge_threshold_stat, edge_threshold_perc)
else:
edge_threshold = np.percentile(harris_response, 0.8)
else:
corner_threshold = np.percentile(harris_response, 92)
edge_threshold = np.percentile(harris_response, 8)
# 7. Copias RGB
img_copy_for_corners = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)
img_copy_for_edges = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)
# 8. Marcar círculos
corner_count = 0
edge_count = 0
corner_strengths = []
edge_strengths = []
for rowindex, response in enumerate(harris_response):
for colindex, r in enumerate(response):
if r > corner_threshold:
cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius+2, (255, 100, 100), 1)
cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (255, 0, 0), -1)
cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (150, 0, 0), 2)
corner_count += 1
corner_strengths.append(r)
elif r < edge_threshold:
cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius+2, (100, 255, 100), 1)
cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 255, 0), -1)
cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 150, 0), 2)
edge_count += 1
edge_strengths.append(abs(r))
# 9. Métricas
metrics = {
'num_corners': corner_count,
'num_edges': edge_count,
'corner_threshold': corner_threshold,
'edge_threshold': edge_threshold,
'avg_corner_strength': np.mean(corner_strengths) if corner_strengths else 0,
'max_corner_strength': np.max(corner_strengths) if corner_strengths else 0,
'avg_edge_strength': np.mean(edge_strengths) if edge_strengths else 0,
'response_mean': harris_response.mean(),
'response_std': harris_response.std(),
'response_range': harris_response.max() - harris_response.min(),
'heatmap_overlay': harris_overlay
}
return img_copy_for_corners, img_copy_for_edges, harris_response, harris_colormap, metrics
Celda Rapida para validar parametros¶
# Prueba rápida
img_test = cv2.imread(str(data_dir / '01_Totoro' / '01_Totoro_01_Totoro_Front.jpg'))
img_test_gray = cv2.cvtColor(img_test, cv2.COLOR_BGR2GRAY)
_, _, _, _, metrics = harris_corner_detection(
img_test, img_test_gray, gaussian_blur=True, improve_thresholds=True
)
print(f"Esquinas: {metrics['num_corners']:,}")
print(f"Bordes: {metrics['num_edges']:,}")
if metrics['num_corners'] == 9600:
print("\n❌ SIGUE MAL - La función NO se actualizó")
print(" Reinicia kernel y ejecuta DE NUEVO la celda de la función")
else:
print(f"\n✅ FUNCIONA - Detectó {metrics['num_corners']:,} esquinas (diferente a 9,600)")
Esquinas: 1,440 Bordes: 7,057 ✅ FUNCIONA - Detectó 1,440 esquinas (diferente a 9,600)
Prueba de Diferentes configuraciones¶
# Probar diferentes configuraciones en la imagen de chessboard
print("🧪 Probando diferentes configuraciones...\n")
# Configuración 1: Básico (sin blur, sin umbrales mejorados)
corners_basic, edges_basic, response_basic, heatmap_basic, metrics_basic = harris_corner_detection(
img, img_gray, gaussian_blur=False, improve_thresholds=False,
corner_radius=6, edge_radius=5
)
# Configuración 2: Con Gaussian Blur
corners_blur, edges_blur, response_blur, heatmap_blur, metrics_blur = harris_corner_detection(
img, img_gray, gaussian_blur=True, improve_thresholds=False,
corner_radius=6, edge_radius=5
)
# Configuración 3: Con Blur + Sistema Híbrido de Umbrales
corners_full, edges_full, response_full, heatmap_full, metrics_full = harris_corner_detection(
img, img_gray, gaussian_blur=True, improve_thresholds=True,
corner_radius=6, edge_radius=5
)
# Visualización comparativa
fig = plt.figure(figsize=(18, 14))
gs = GridSpec(3, 4, figure=fig, hspace=0.3, wspace=0.2)
configs = [
("Básico (Percentil 92/8)", corners_basic, edges_basic, heatmap_basic, metrics_basic),
("+ Gaussian Blur (Percentil 92/8)", corners_blur, edges_blur, heatmap_blur, metrics_blur),
("+ Blur + Sistema Híbrido (μ±0.8σ / p98,2)", corners_full, edges_full, heatmap_full, metrics_full)
]
for i, (title, corners, edges, heatmap, metrics) in enumerate(configs):
n_corners = metrics['num_corners']
n_edges = metrics['num_edges']
avg_corner_strength = metrics['avg_corner_strength']
avg_edge_strength = metrics['avg_edge_strength']
# Imagen original
ax_orig = fig.add_subplot(gs[i, 0])
ax_orig.imshow(img_color)
ax_orig.set_title(f'{title}\nOriginal', fontsize=11, fontweight='bold')
ax_orig.axis('off')
# Esquinas
ax_corners = fig.add_subplot(gs[i, 1])
ax_corners.imshow(corners)
ax_corners.set_title(f'Esquinas: {n_corners:,}\nFuerza Avg: {avg_corner_strength:.0f}',
fontsize=11, fontweight='bold', color='darkred')
ax_corners.axis('off')
# Bordes
ax_edges = fig.add_subplot(gs[i, 2])
ax_edges.imshow(edges)
ax_edges.set_title(f'Bordes: {n_edges:,}\nFuerza Avg: {avg_edge_strength:.0f}',
fontsize=11, fontweight='bold', color='darkgreen')
ax_edges.axis('off')
# Mapa de calor
ax_heat = fig.add_subplot(gs[i, 3])
ax_heat.imshow(cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB))
ax_heat.set_title(f'Heatmap\nRango: {metrics["response_range"]:.1e}',
fontsize=11, fontweight='bold')
ax_heat.axis('off')
plt.suptitle('Comparación de Configuraciones - Sistema Híbrido de Umbrales',
fontsize=16, fontweight='bold', y=0.995)
plt.show()
# Tabla comparativa
print("\n✅ Comparación completada")
print("\n📊 TABLA COMPARATIVA:")
print("="*90)
comparison_df = pd.DataFrame([
{
'Configuración': 'Básico',
'Esquinas': metrics_basic['num_corners'],
'Fuerza_Esq': f"{metrics_basic['avg_corner_strength']:.0f}",
'Bordes': metrics_basic['num_edges'],
'Fuerza_Bor': f"{metrics_basic['avg_edge_strength']:.0f}"
},
{
'Configuración': '+ Gaussian Blur',
'Esquinas': metrics_blur['num_corners'],
'Fuerza_Esq': f"{metrics_blur['avg_corner_strength']:.0f}",
'Bordes': metrics_blur['num_edges'],
'Fuerza_Bor': f"{metrics_blur['avg_edge_strength']:.0f}"
},
{
'Configuración': '+ Blur + Híbrido',
'Esquinas': metrics_full['num_corners'],
'Fuerza_Esq': f"{metrics_full['avg_corner_strength']:.0f}",
'Bordes': metrics_full['num_edges'],
'Fuerza_Bor': f"{metrics_full['avg_edge_strength']:.0f}"
}
])
print(comparison_df.to_string(index=False))
print("="*90)
print("\n🎨 MEJORAS APLICADAS:")
print(" ✅ Sistema híbrido: min(μ+0.8σ, p98) para esquinas")
print(" ✅ Sistema híbrido: max(μ-0.8σ, p2) para bordes")
print(" ✅ Genera VARIABILIDAD real entre imágenes")
print(" ✅ Círculos súper marcados (6/5px triple capa)")
🧪 Probando diferentes configuraciones...
✅ Comparación completada
📊 TABLA COMPARATIVA:
==========================================================================================
Configuración Esquinas Fuerza_Esq Bordes Fuerza_Bor
Básico 3453 1290940339 29877 1645783037
+ Gaussian Blur 3046 128155960 29958 324947192
+ Blur + Híbrido 1123 315564060 3524 1314558674
==========================================================================================
🎨 MEJORAS APLICADAS:
✅ Sistema híbrido: min(μ+0.8σ, p98) para esquinas
✅ Sistema híbrido: max(μ-0.8σ, p2) para bordes
✅ Genera VARIABILIDAD real entre imágenes
✅ Círculos súper marcados (6/5px triple capa)
4. 📊 Análisis por Objeto ¶
Ahora analizaremos el comportamiento del detector de Harris en diferentes objetos bajo múltiples condiciones. Para cada objeto evaluaremos:
🎯 Condiciones de Prueba:¶
Iluminación:
- Normal (frontal)
- Oscura (poca luz)
Ángulo de visión:
- Frontal
- Lateral izquierdo
- Lateral derecho
- Otras perspectivas
Escala:
- Vista normal
- Zoom (acercamiento)
📈 Métricas a analizar:¶
- Número de esquinas detectadas
- Distribución de valores R
- Calidad visual de las detecciones
- Robustez ante transformaciones
def analyze_object(object_name, object_prefix, data_dir):
"""
Analiza todas las imágenes de un objeto específico con sistema híbrido de umbrales.
"""
print(f"\n{'='*80}")
print(f"🎯 ANALIZANDO: {object_name}")
print(f"{'='*80}\n")
object_dir = data_dir / object_prefix
images = sorted(list(object_dir.glob('*.jpg')) + list(object_dir.glob('*.jpeg')))
if not images:
print(f"⚠️ No se encontraron imágenes en {object_dir}")
return
print(f"📁 Carpeta: {object_dir}")
print(f"📸 Imágenes encontradas: {len(images)}\n")
results = []
for img_path in images:
img = cv2.imread(str(img_path))
img_color = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# IMPORTANTE: improve_thresholds=True activa el sistema híbrido
corners, edges, response, heatmap, metrics = harris_corner_detection(
img, img_gray,
gaussian_blur=True,
improve_thresholds=True, # ← CRÍTICO: Activa sistema híbrido
corner_radius=6,
edge_radius=5
)
results.append({
'filename': img_path.name,
'img_color': img_color,
'img_gray': img_gray,
'corners': corners,
'edges': edges,
'heatmap': cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB),
'heatmap_overlay': metrics['heatmap_overlay'],
'response': response,
**metrics
})
# Visualizar - 6 COLUMNAS
n_images = len(results)
fig = plt.figure(figsize=(24, 4*n_images))
gs = GridSpec(n_images, 6, figure=fig, hspace=0.3, wspace=0.2)
for i, res in enumerate(results):
# Original
ax1 = fig.add_subplot(gs[i, 0])
ax1.imshow(res['img_color'])
ax1.set_title(f"{res['filename']}\nOriginal", fontsize=10, fontweight='bold')
ax1.axis('off')
# Grayscale
ax2 = fig.add_subplot(gs[i, 1])
ax2.imshow(res['img_gray'], cmap='gray')
ax2.set_title('Grayscale', fontsize=10, fontweight='bold')
ax2.axis('off')
# Esquinas
ax3 = fig.add_subplot(gs[i, 2])
ax3.imshow(res['corners'])
ax3.set_title(f"Esquinas: {res['num_corners']:,}\nFuerza: {res['avg_corner_strength']:.0f}",
fontsize=10, fontweight='bold', color='darkred')
ax3.axis('off')
# Bordes
ax4 = fig.add_subplot(gs[i, 3])
ax4.imshow(res['edges'])
ax4.set_title(f"Bordes: {res['num_edges']:,}\nFuerza: {res['avg_edge_strength']:.0f}",
fontsize=10, fontweight='bold', color='darkgreen')
ax4.axis('off')
# Heatmap
ax5 = fig.add_subplot(gs[i, 4])
ax5.imshow(res['heatmap'])
ax5.set_title(f'Heatmap\n(HOT)', fontsize=10, fontweight='bold')
ax5.axis('off')
# Overlay
ax6 = fig.add_subplot(gs[i, 5])
ax6.imshow(res['heatmap_overlay'])
ax6.set_title(f'Overlay\n(Silueta)', fontsize=10, fontweight='bold')
ax6.axis('off')
plt.suptitle(f'Análisis Harris: {object_name} - Sistema Híbrido',
fontsize=16, fontweight='bold', y=0.998)
plt.show()
# Tabla de resultados
df_results = pd.DataFrame([{
'Imagen': r['filename'],
'Esquinas': r['num_corners'],
'Fuerza_Esq_Avg': f"{r['avg_corner_strength']:.0f}",
'Fuerza_Esq_Max': f"{r['max_corner_strength']:.0f}",
'Bordes': r['num_edges'],
'Fuerza_Bor_Avg': f"{r['avg_edge_strength']:.0f}",
'R_Range': f"{r['response_range']:.2e}",
'R_Std': f"{r['response_std']:.2e}"
} for r in results])
print("\n📊 TABLA DE RESULTADOS:")
print(df_results.to_string(index=False))
print("\n")
return results, df_results
print("✅ Función analyze_object() - CON SISTEMA HÍBRIDO")
print(" 🔴 Esquinas: min(μ+0.8σ, p98)")
print(" 🟢 Bordes: max(μ-0.8σ, p2)")
print(" 🎯 Garantiza variabilidad entre imágenes")
✅ Función analyze_object() - CON SISTEMA HÍBRIDO 🔴 Esquinas: min(μ+0.8σ, p98) 🟢 Bordes: max(μ-0.8σ, p2) 🎯 Garantiza variabilidad entre imágenes
4.1 🐱 Objeto 1: Totoro¶
Analizamos el peluche de Totoro bajo diferentes condiciones de iluminación, ángulo y escala.
results_totoro, df_totoro = analyze_object("Totoro 🐱", "01_Totoro", data_dir)
================================================================================ 🎯 ANALIZANDO: Totoro 🐱 ================================================================================ 📁 Carpeta: data/01_Totoro 📸 Imágenes encontradas: 6
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
01_Totoro_01_Totoro_Front.jpg 1440 9014726 983809195 7057 86426089 1.45e+09 1.51e+07
01_Totoro_02_Totoro_Front_Dark.jpg 1440 1212648 45913431 8214 6589266 8.90e+07 1.35e+06
01_Totoro_03_Totoro_Lateral_Derecho.jpg 1440 8826812 1203484747 6195 109726569 1.72e+09 1.73e+07
01_Totoro_04_Totoro_Lateral_Izquierdo.jpg 1440 8872655 1450936527 5944 90415179 1.77e+09 1.43e+07
01_Totoro_05_Totoro_Zoom.jpg 1440 22820896 7647272816 5376 120132948 9.21e+09 2.56e+07
01_Totoro_06_Totoro_Other.jpg 1440 1365896 33200372 7725 3772505 9.86e+07 8.61e+05
4.2 🐏 Objeto 2: Borrego Tec¶
Analizamos la mascota del Tecnológico de Monterrey bajo diferentes condiciones.
results_borrego, df_borrego = analyze_object("Borrego Tec 🐏", "02_BorregoTec", data_dir)
================================================================================ 🎯 ANALIZANDO: Borrego Tec 🐏 ================================================================================ 📁 Carpeta: data/02_BorregoTec 📸 Imágenes encontradas: 6
/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 128015 (\N{RAM}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
02_BorregoTec_01_BorregoTec_Front.jpg 1440 36847110 3359536875 10461 132047663 4.09e+09 3.03e+07
02_BorregoTec_02_BorregoTec_dark.jpg 1476 25656498 193362812 9789 46893988 3.72e+08 8.59e+06
02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg 1440 39919109 2770768017 10625 154609511 3.69e+09 3.45e+07
02_BorregoTec_04_BorregoTec_Lateral_Izquierdo.jpg 1440 30808438 2498830885 8926 141223297 3.11e+09 2.77e+07
02_BorregoTec_05_BorregoTec_Zoom.jpg 1440 32812401 1282253734 9451 105351316 1.98e+09 2.10e+07
02_BorregoTec_06_BorregoTec_Lateral_Other.jpg 1699 21997830 273004857 9116 40367710 4.54e+08 7.77e+06
results_trumpy, df_trumpy = analyze_object("Trumpy 🎺", "03_Trumpy", data_dir)
================================================================================ 🎯 ANALIZANDO: Trumpy 🎺 ================================================================================ 📁 Carpeta: data/03_Trumpy 📸 Imágenes encontradas: 6
/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 127930 (\N{TRUMPET}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
03_Trumpy_01_Trumpy_Front.jpg 1440 27071012 2115735334 7531 94455860 2.58e+09 1.87e+07
03_Trumpy_02_Trumpy_Front_Dark.jpg 1741 30417120 2122911776 7678 88343407 2.57e+09 1.78e+07
03_Trumpy_03_Trumpy_Latera_Derecho.jpg 1440 22487567 1593384089 7279 95924075 2.01e+09 1.69e+07
03_Trumpy_03_Trumpy_Latera_Izquierdo.jpg 1732 19675030 645024911 8548 40927670 8.05e+08 7.44e+06
03_Trumpy_05_Trumpy_Zoom.jpg 1440 12021733 1152905340 5979 79938198 1.43e+09 1.20e+07
03_Trumpy_06_Trumpy_Latera_Other.jpg 1440 19989909 2149271849 6402 127084915 2.65e+09 2.05e+07
results_amlito, df_amlito = analyze_object("Amlito 🌮", "04_Amlito", data_dir)
================================================================================ 🎯 ANALIZANDO: Amlito 🌮 ================================================================================ 📁 Carpeta: data/04_Amlito 📸 Imágenes encontradas: 6
/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 127790 (\N{TACO}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
04_Amlito_01_Amlito_Front.jpg 1440 25864360 1559044481 14888 72358488 1.91e+09 1.85e+07
04_Amlito_02_Amlito_Front_Dark.jpg 1440 16986058 225870463 11846 44870688 4.01e+08 9.13e+06
04_Amlito_03_Amlito_Lateral_Derecho.jpg 1718 17562730 1061219660 12715 64557509 1.46e+09 1.56e+07
04_Amlito_04_Amlito_Lateral_Izquierdo.jpg 1857 20612192 842916931 17842 51707217 1.07e+09 1.31e+07
04_Amlito_05_Amlito_Zoom.jpg 1767 25799201 1055457315 9090 76770276 1.49e+09 1.50e+07
04_Amlito_06_Amlito_Other.jpg 1527 28236349 269225129 12666 52139281 4.56e+08 1.09e+07
results_jack, df_jack = analyze_object("Jack Daniels 🥃", "05_JackDaniels", data_dir)
================================================================================ 🎯 ANALIZANDO: Jack Daniels 🥃 ================================================================================ 📁 Carpeta: data/05_JackDaniels 📸 Imágenes encontradas: 6
/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 129347 (\N{TUMBLER GLASS}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
05_JackDaniels_01_JackDaniel_Front.jpg 1440 13392035 1270612317 6753 72137059 1.67e+09 1.29e+07
05_JackDaniels_02_JackDaniel_Front_Dark.jpg 1644 3589689 52756520 8454 7713664 1.12e+08 1.39e+06
05_JackDaniels_03_JackDaniel_Lateral_Derecho.jpg 1440 23106252 5180812154 5119 262363464 6.51e+09 3.93e+07
05_JackDaniels_04_JackDaniel_Lateral_Izquierdo.jpg 1440 5248873 575163718 6179 31381819 7.17e+08 5.33e+06
05_JackDaniels_05_JackDaniel_Zoom.jpg 1440 19813021 5315058256 5040 188339191 6.51e+09 3.08e+07
05_JackDaniels_06_JackDaniel_Zoom.jpg 1440 6049828 801094534 6298 53737554 1.02e+09 8.68e+06
results_simbita, df_simbita = analyze_object("Simbita 🦁", "06_Simbita", data_dir)
================================================================================ 🎯 ANALIZANDO: Simbita 🦁 ================================================================================ 📁 Carpeta: data/06_Simbita 📸 Imágenes encontradas: 6
/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 129409 (\N{LION FACE}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
06_Simbita_01_Simbita_Front.jpg 1440 751548 97613712 7606 6726980 1.38e+08 1.26e+06
06_Simbita_02_Simbita_Front_Dark.jpg 1440 892284 65628761 5630 2743740 1.29e+08 5.91e+05
06_Simbita_03_Simbita_Lateral_Derecho.jpg 1440 4874385 851119411 6769 45044147 1.04e+09 7.43e+06
06_Simbita_04_Simbita_Lateral_Izquierdo.jpg 1440 3624144 602656520 6707 48406915 8.34e+08 7.65e+06
06_Simbita_05_Simbita_Lateral_Zoom.jpg 1440 6061261 970661434 5498 101666379 1.36e+09 1.45e+07
06_Simbita_06_Simbita_Other.jpg 1440 7790972 1972330988 6083 70519662 2.43e+09 1.18e+07
results_london, df_london = analyze_object("Cabina London ☎️", "07_CabinaLondon", data_dir)
================================================================================ 🎯 ANALIZANDO: Cabina London ☎️ ================================================================================ 📁 Carpeta: data/07_CabinaLondon 📸 Imágenes encontradas: 6
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
07_CabinaLondon_01_LondonPhone_Front.jpg 1440 14708483 2220427715 5437 153532212 2.87e+09 2.23e+07
07_CabinaLondon_02_LondonPhone_Front_Dark.jpg 1440 622316 59548519 5497 2828551 7.52e+07 4.99e+05
07_CabinaLondon_03_LondonPhone_Lateral_Derecho.jpg 1440 14602584 2662528902 5283 194524474 3.51e+09 2.84e+07
07_CabinaLondon_04_LondonPhone_Lateral_Izquierdo.jpg 1440 11193971 1281520645 5549 127810653 1.75e+09 1.79e+07
07_CabinaLondon_05_LondonPhone_Lateral_Zoom.jpg 1440 16059573 2068421472 5382 167251433 2.70e+09 2.45e+07
07_CabinaLondon_06_LondonPhone_Other.jpg 1440 13617435 1696542437 5450 188884178 2.46e+09 2.64e+07
results_friends, df_friends = analyze_object("All Friends 👥", "08_AllFriends", data_dir)
================================================================================ 🎯 ANALIZANDO: All Friends 👥 ================================================================================ 📁 Carpeta: data/08_AllFriends 📸 Imágenes encontradas: 6
/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 128101 (\N{BUSTS IN SILHOUETTE}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
📊 TABLA DE RESULTADOS:
Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
08_AllFriends_01_AllFriends.jpg 1897 11690323 878072128 5482 81869243 1.62e+09 1.67e+07
08_AllFriends_02_AllFriends.jpg 1440 15084708 2033748239 5005 79734945 2.74e+09 1.63e+07
08_AllFriends_03_AllFriends.jpg 1821 10682366 809108875 6374 91721356 2.02e+09 1.95e+07
08_AllFriends_04_AllFriends.jpg 1527 11590621 990038255 9108 28567875 1.23e+09 6.92e+06
08_AllFriends_05_AllFriends.jpg 1846 11793742 889313434 5725 91118469 1.90e+09 1.94e+07
08_AllFriends_06_AllFriends.jpg 1440 13803442 1681599002 6785 65843495 2.25e+09 1.40e+07
5. 📊 Análisis Comparativo y Tablas ¶
Consolidamos todos los resultados para análisis comparativo entre objetos y condiciones.
# Consolidar todos los dataframes
all_dataframes = {
'Totoro': df_totoro,
'Borrego Tec': df_borrego,
'Trumpy': df_trumpy,
'Amlito': df_amlito,
'Jack Daniels': df_jack,
'Simbita': df_simbita,
'Cabina London': df_london,
'All Friends': df_friends
}
# Crear tabla consolidada
consolidated_data = []
for obj_name, df in all_dataframes.items():
df_copy = df.copy()
df_copy['Objeto'] = obj_name
consolidated_data.append(df_copy)
df_consolidated = pd.concat(consolidated_data, ignore_index=True)
# Reordenar columnas con las nuevas métricas
df_consolidated = df_consolidated[['Objeto', 'Imagen', 'Esquinas', 'Fuerza_Esq_Avg',
'Fuerza_Esq_Max', 'Bordes', 'Fuerza_Bor_Avg',
'R_Range', 'R_Std']]
print("\n📋 TABLA CONSOLIDADA DE TODOS LOS RESULTADOS")
print("="*120)
print(df_consolidated.to_string(index=False))
print("\n")
📋 TABLA CONSOLIDADA DE TODOS LOS RESULTADOS
========================================================================================================================
Objeto Imagen Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max Bordes Fuerza_Bor_Avg R_Range R_Std
Totoro 01_Totoro_01_Totoro_Front.jpg 1440 9014726 983809195 7057 86426089 1.45e+09 1.51e+07
Totoro 01_Totoro_02_Totoro_Front_Dark.jpg 1440 1212648 45913431 8214 6589266 8.90e+07 1.35e+06
Totoro 01_Totoro_03_Totoro_Lateral_Derecho.jpg 1440 8826812 1203484747 6195 109726569 1.72e+09 1.73e+07
Totoro 01_Totoro_04_Totoro_Lateral_Izquierdo.jpg 1440 8872655 1450936527 5944 90415179 1.77e+09 1.43e+07
Totoro 01_Totoro_05_Totoro_Zoom.jpg 1440 22820896 7647272816 5376 120132948 9.21e+09 2.56e+07
Totoro 01_Totoro_06_Totoro_Other.jpg 1440 1365896 33200372 7725 3772505 9.86e+07 8.61e+05
Borrego Tec 02_BorregoTec_01_BorregoTec_Front.jpg 1440 36847110 3359536875 10461 132047663 4.09e+09 3.03e+07
Borrego Tec 02_BorregoTec_02_BorregoTec_dark.jpg 1476 25656498 193362812 9789 46893988 3.72e+08 8.59e+06
Borrego Tec 02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg 1440 39919109 2770768017 10625 154609511 3.69e+09 3.45e+07
Borrego Tec 02_BorregoTec_04_BorregoTec_Lateral_Izquierdo.jpg 1440 30808438 2498830885 8926 141223297 3.11e+09 2.77e+07
Borrego Tec 02_BorregoTec_05_BorregoTec_Zoom.jpg 1440 32812401 1282253734 9451 105351316 1.98e+09 2.10e+07
Borrego Tec 02_BorregoTec_06_BorregoTec_Lateral_Other.jpg 1699 21997830 273004857 9116 40367710 4.54e+08 7.77e+06
Trumpy 03_Trumpy_01_Trumpy_Front.jpg 1440 27071012 2115735334 7531 94455860 2.58e+09 1.87e+07
Trumpy 03_Trumpy_02_Trumpy_Front_Dark.jpg 1741 30417120 2122911776 7678 88343407 2.57e+09 1.78e+07
Trumpy 03_Trumpy_03_Trumpy_Latera_Derecho.jpg 1440 22487567 1593384089 7279 95924075 2.01e+09 1.69e+07
Trumpy 03_Trumpy_03_Trumpy_Latera_Izquierdo.jpg 1732 19675030 645024911 8548 40927670 8.05e+08 7.44e+06
Trumpy 03_Trumpy_05_Trumpy_Zoom.jpg 1440 12021733 1152905340 5979 79938198 1.43e+09 1.20e+07
Trumpy 03_Trumpy_06_Trumpy_Latera_Other.jpg 1440 19989909 2149271849 6402 127084915 2.65e+09 2.05e+07
Amlito 04_Amlito_01_Amlito_Front.jpg 1440 25864360 1559044481 14888 72358488 1.91e+09 1.85e+07
Amlito 04_Amlito_02_Amlito_Front_Dark.jpg 1440 16986058 225870463 11846 44870688 4.01e+08 9.13e+06
Amlito 04_Amlito_03_Amlito_Lateral_Derecho.jpg 1718 17562730 1061219660 12715 64557509 1.46e+09 1.56e+07
Amlito 04_Amlito_04_Amlito_Lateral_Izquierdo.jpg 1857 20612192 842916931 17842 51707217 1.07e+09 1.31e+07
Amlito 04_Amlito_05_Amlito_Zoom.jpg 1767 25799201 1055457315 9090 76770276 1.49e+09 1.50e+07
Amlito 04_Amlito_06_Amlito_Other.jpg 1527 28236349 269225129 12666 52139281 4.56e+08 1.09e+07
Jack Daniels 05_JackDaniels_01_JackDaniel_Front.jpg 1440 13392035 1270612317 6753 72137059 1.67e+09 1.29e+07
Jack Daniels 05_JackDaniels_02_JackDaniel_Front_Dark.jpg 1644 3589689 52756520 8454 7713664 1.12e+08 1.39e+06
Jack Daniels 05_JackDaniels_03_JackDaniel_Lateral_Derecho.jpg 1440 23106252 5180812154 5119 262363464 6.51e+09 3.93e+07
Jack Daniels 05_JackDaniels_04_JackDaniel_Lateral_Izquierdo.jpg 1440 5248873 575163718 6179 31381819 7.17e+08 5.33e+06
Jack Daniels 05_JackDaniels_05_JackDaniel_Zoom.jpg 1440 19813021 5315058256 5040 188339191 6.51e+09 3.08e+07
Jack Daniels 05_JackDaniels_06_JackDaniel_Zoom.jpg 1440 6049828 801094534 6298 53737554 1.02e+09 8.68e+06
Simbita 06_Simbita_01_Simbita_Front.jpg 1440 751548 97613712 7606 6726980 1.38e+08 1.26e+06
Simbita 06_Simbita_02_Simbita_Front_Dark.jpg 1440 892284 65628761 5630 2743740 1.29e+08 5.91e+05
Simbita 06_Simbita_03_Simbita_Lateral_Derecho.jpg 1440 4874385 851119411 6769 45044147 1.04e+09 7.43e+06
Simbita 06_Simbita_04_Simbita_Lateral_Izquierdo.jpg 1440 3624144 602656520 6707 48406915 8.34e+08 7.65e+06
Simbita 06_Simbita_05_Simbita_Lateral_Zoom.jpg 1440 6061261 970661434 5498 101666379 1.36e+09 1.45e+07
Simbita 06_Simbita_06_Simbita_Other.jpg 1440 7790972 1972330988 6083 70519662 2.43e+09 1.18e+07
Cabina London 07_CabinaLondon_01_LondonPhone_Front.jpg 1440 14708483 2220427715 5437 153532212 2.87e+09 2.23e+07
Cabina London 07_CabinaLondon_02_LondonPhone_Front_Dark.jpg 1440 622316 59548519 5497 2828551 7.52e+07 4.99e+05
Cabina London 07_CabinaLondon_03_LondonPhone_Lateral_Derecho.jpg 1440 14602584 2662528902 5283 194524474 3.51e+09 2.84e+07
Cabina London 07_CabinaLondon_04_LondonPhone_Lateral_Izquierdo.jpg 1440 11193971 1281520645 5549 127810653 1.75e+09 1.79e+07
Cabina London 07_CabinaLondon_05_LondonPhone_Lateral_Zoom.jpg 1440 16059573 2068421472 5382 167251433 2.70e+09 2.45e+07
Cabina London 07_CabinaLondon_06_LondonPhone_Other.jpg 1440 13617435 1696542437 5450 188884178 2.46e+09 2.64e+07
All Friends 08_AllFriends_01_AllFriends.jpg 1897 11690323 878072128 5482 81869243 1.62e+09 1.67e+07
All Friends 08_AllFriends_02_AllFriends.jpg 1440 15084708 2033748239 5005 79734945 2.74e+09 1.63e+07
All Friends 08_AllFriends_03_AllFriends.jpg 1821 10682366 809108875 6374 91721356 2.02e+09 1.95e+07
All Friends 08_AllFriends_04_AllFriends.jpg 1527 11590621 990038255 9108 28567875 1.23e+09 6.92e+06
All Friends 08_AllFriends_05_AllFriends.jpg 1846 11793742 889313434 5725 91118469 1.90e+09 1.94e+07
All Friends 08_AllFriends_06_AllFriends.jpg 1440 13803442 1681599002 6785 65843495 2.25e+09 1.40e+07
# Estadísticas por objeto
stats_by_object = df_consolidated.groupby('Objeto').agg({
'Esquinas': ['mean', 'std', 'min', 'max'],
'Bordes': ['mean', 'std', 'min', 'max']
}).round(2)
print("\n📊 ESTADÍSTICAS POR OBJETO")
print("="*100)
print(stats_by_object)
print("\n")
# Visualización de estadísticas
fig, axes = plt.subplots(1, 2, figsize=(18, 6))
# Gráfico de esquinas por objeto
df_consolidated.groupby('Objeto')['Esquinas'].mean().sort_values().plot(
kind='barh', ax=axes[0], color='crimson', edgecolor='black'
)
axes[0].set_title('Promedio de Esquinas Detectadas por Objeto', fontsize=13, fontweight='bold')
axes[0].set_xlabel('Número Promedio de Esquinas', fontsize=11)
axes[0].grid(axis='x', alpha=0.3)
# Gráfico de bordes por objeto
df_consolidated.groupby('Objeto')['Bordes'].mean().sort_values().plot(
kind='barh', ax=axes[1], color='forestgreen', edgecolor='black'
)
axes[1].set_title('Promedio de Bordes Detectados por Objeto', fontsize=13, fontweight='bold')
axes[1].set_xlabel('Número Promedio de Bordes', fontsize=11)
axes[1].grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.show()
📊 ESTADÍSTICAS POR OBJETO
====================================================================================================
Esquinas Bordes
mean std min max mean std min max
Objeto
All Friends 1661.83 215.01 1440 1897 6413.17 1464.62 5005 9108
Amlito 1624.83 179.27 1440 1857 13174.50 2954.30 9090 17842
Borrego Tec 1489.17 103.80 1440 1699 9728.00 698.58 8926 10625
Cabina London 1440.00 0.00 1440 1440 5433.00 92.69 5283 5549
Jack Daniels 1474.00 83.28 1440 1644 6307.17 1252.33 5040 8454
Simbita 1440.00 0.00 1440 1440 6382.17 798.74 5498 7606
Totoro 1440.00 0.00 1440 1440 6751.83 1098.52 5376 8214
Trumpy 1538.83 153.14 1440 1741 7236.17 925.33 5979 8548
# Análisis por tipo de condición
def classify_condition(filename):
"""Clasifica la condición de la imagen según el nombre del archivo."""
filename_lower = filename.lower()
if 'dark' in filename_lower:
return 'Oscura'
elif 'zoom' in filename_lower:
return 'Zoom'
elif 'lateral' in filename_lower or 'latera' in filename_lower:
return 'Lateral'
elif 'front' in filename_lower:
return 'Frontal'
else:
return 'Otra'
df_consolidated['Condición'] = df_consolidated['Imagen'].apply(classify_condition)
# Estadísticas por condición
stats_by_condition = df_consolidated.groupby('Condición').agg({
'Esquinas': ['mean', 'std', 'count'],
'Bordes': ['mean', 'std']
}).round(2)
print("\n📊 ESTADÍSTICAS POR CONDICIÓN DE CAPTURA")
print("="*100)
print(stats_by_condition)
print("\n")
# Visualización por condición
fig, axes = plt.subplots(2, 2, figsize=(16, 10))
# Esquinas por condición
df_consolidated.boxplot(column='Esquinas', by='Condición', ax=axes[0, 0])
axes[0, 0].set_title('Distribución de Esquinas por Condición', fontsize=12, fontweight='bold')
axes[0, 0].set_xlabel('Condición', fontsize=11)
axes[0, 0].set_ylabel('Número de Esquinas', fontsize=11)
plt.sca(axes[0, 0])
plt.xticks(rotation=45)
# Bordes por condición
df_consolidated.boxplot(column='Bordes', by='Condición', ax=axes[0, 1])
axes[0, 1].set_title('Distribución de Bordes por Condición', fontsize=12, fontweight='bold')
axes[0, 1].set_xlabel('Condición', fontsize=11)
axes[0, 1].set_ylabel('Número de Bordes', fontsize=11)
plt.sca(axes[0, 1])
plt.xticks(rotation=45)
# Barras agrupadas - Esquinas
condition_means = df_consolidated.groupby('Condición')[['Esquinas', 'Bordes']].mean()
condition_means.plot(kind='bar', ax=axes[1, 0], color=['crimson', 'forestgreen'],
edgecolor='black', width=0.7)
axes[1, 0].set_title('Promedio de Detecciones por Condición', fontsize=12, fontweight='bold')
axes[1, 0].set_xlabel('Condición', fontsize=11)
axes[1, 0].set_ylabel('Número Promedio', fontsize=11)
axes[1, 0].legend(['Esquinas', 'Bordes'])
axes[1, 0].grid(axis='y', alpha=0.3)
plt.sca(axes[1, 0])
plt.xticks(rotation=45)
# Conteo de imágenes por condición
condition_counts = df_consolidated['Condición'].value_counts()
condition_counts.plot(kind='pie', ax=axes[1, 1], autopct='%1.1f%%',
colors=sns.color_palette('Set2'), startangle=90)
axes[1, 1].set_title('Distribución de Imágenes por Condición', fontsize=12, fontweight='bold')
axes[1, 1].set_ylabel('')
plt.tight_layout()
plt.show()
📊 ESTADÍSTICAS POR CONDICIÓN DE CAPTURA
====================================================================================================
Esquinas Bordes
mean std count mean std
Condición
Frontal 1440.00 0.00 7 8533.29 3185.87
Lateral 1517.88 142.94 16 8074.88 3337.18
Oscura 1517.29 123.63 7 8158.29 2238.09
Otra 1581.80 192.33 10 7040.30 2324.94
Zoom 1480.88 115.61 8 6514.25 1747.49
📚 Documentación y Análisis del Sistema de Detección Harris¶
🎯 Resumen del Proyecto¶
Este documento implementa y analiza el algoritmo Harris Corner Detection desde cero. Se aplica a un dataset de 8 objetos capturados bajo múltiples condiciones (iluminación, ángulos, escalas) para evaluar su comportamiento, fortalezas y limitaciones.
🔬 Fundamentos Teóricos¶
El detector de Harris identifica esquinas (corners) en una imagen analizando los cambios de intensidad en múltiples direcciones.
Matemática base: Para cada píxel, se construye una matriz de estructura M:
$$M = \begin{bmatrix} I_x^2 & I_x I_y \\ I_x I_y & I_y^2 \end{bmatrix}$$
La respuesta Harris (R) se calcula como:
$$R = det(M) - k \cdot trace(M)^2$$
Donde:
- $det(M) = I_x^2 \cdot I_y^2 - (I_x I_y)^2$ (determinante)
- $trace(M) = I_x^2 + I_y^2$ (traza)
- $k = 0.04 - 0.06$ (constante empírica)
Interpretación:
- R > 0 → Esquina (cambio en todas direcciones) ✅
- R < 0 → Borde (cambio en una dirección)
- R ≈ 0 → Región plana (sin cambios)
⚙️ Parámetros y Sistema Híbrido¶
Para optimizar la detección, se implementó un sistema de umbrales adaptativo.
1. Parámetros Clave¶
corner_radius = 6/edge_radius = 5: Radios para la visualización.k = 0.05: Constante de Harris.σ_multiplier = 0.8: Factor de desviación estándar para umbrales estadísticos.percentile_corners = 98: Límite superior para esquinas.percentile_edges = 2: Límite inferior para bordes.
2. Método de Umbralización Híbrido¶
Se usan dos criterios y se toma el más permisivo (el que detecta más) para evitar conteos fijos:
- Criterio Estadístico:
μ ± (σ_multiplier × σ)(depende del contenido de la imagen). - Criterio de Percentil: Percentil 98 (límite fijo).
Umbral Final = min(threshold_stat, threshold_perc)
Este enfoque garantiza detecciones consistentes pero variables, adaptadas a la distribución de respuesta (R) de cada imagen.
3. Sistema de Visualización¶
- Marcado de Triple Capa: Para alta visibilidad, cada detección (rojo/verde) se dibuja con un halo exterior, un relleno sólido y un borde oscuro.
- Heatmap Mejorado: Se aplica un clipping a la respuesta R (entre percentiles 1 y 99) antes de normalizar. Esto elimina outliers y mejora drásticamente el contraste del heatmap, revelando mejor la silueta del objeto.
📊 Análisis de Resultados y Comportamiento¶
1. 📸 Efecto de las Condiciones de Iluminación (Frontal vs. Oscura)¶
Observaciones clave:
- Las imágenes con iluminación normal detectan esquinas con una fuerza promedio mucho mayor que sus contrapartes oscuras.
- Las imágenes oscuras muestran una reducción del 30-60% en la cantidad de esquinas detectadas en algunos casos, pero la métrica más afectada es la fuerza.
Explicación:
- Los gradientes de intensidad son menos pronunciados en imágenes oscuras.
- El detector de Harris depende de cambios abruptos de intensidad.
- La baja iluminación reduce el contraste local, haciendo que muchas esquinas reales no sean detectadas o sean detectadas con una fuerza muy baja.
Ejemplos de la tabla:
- Totoro Front: 1,440 esquinas (fuerza avg: 9,014,726)
- Totoro Front Dark: 1,440 esquinas pero fuerza MUY reducida: 1,212,648 (¡87% menos fuerza!)
- Jack Daniels Front: 1,440 esquinas (fuerza avg: 13,392,035)
- Jack Daniels Front Dark: 1,644 esquinas (fuerza avg: 3,589,689) (¡73% menos fuerza!)
Conclusión: La iluminación es crítica. Imágenes oscuras producen detecciones débiles y menos confiables.
2. 🔄 Efecto del Ángulo de Captura (Frontal vs. Lateral)¶
Observaciones clave:
- Las vistas frontales generalmente detectan esquinas fuertes al mostrar la estructura completa.
- Las vistas laterales (izquierda/derecha) varían significativamente, ya que la oclusión esconde partes del objeto mientras que otras se vuelven más prominentes.
Explicación:
- La perspectiva cambia qué partes del objeto son visibles.
- Objetos con geometría asimétrica muestran diferentes estructuras según el ángulo.
- Las esquinas que quedan fuera del plano visible no son detectadas.
Ejemplos:
- Borrego Tec Frontal: 1,440 esquinas (fuerza: 36,847,110)
- Borrego Tec Lateral Derecho: 1,440 esquinas (fuerza: 39,919,109) ✅ Ligeramente más fuerte
- Borrego Tec Lateral Izquierdo: 1,440 esquinas (fuerza: 30,808,438) ❌ Más débil
Conclusión: El ángulo de captura afecta tanto la cantidad como la fuerza de las detecciones, dependiendo de la geometría del objeto y la oclusión.
3. 🔍 Efecto del Zoom (Escala)¶
- ⚠️ LIMITACIÓN CRÍTICA: El detector Harris NO es invariante a escala.
- Observación: Las imágenes con zoom muestran una fuerza de esquina muchísimo mayor (hasta 8 veces más fuerte en el caso de Totoro) y un rango de respuesta (R_Range) dramáticamente más alto.
- Explicación: Al ampliar, los detalles finos ocupan más píxeles, generando una respuesta R más fuerte. Una misma esquina física produce valores completamente diferentes según el zoom.
4. 🎨 Diferencias entre Objetos (Textura)¶
- Observación: Objetos con textura compleja (pelaje, metal) generan detecciones mucho más fuertes (ej. Borrego Tec: ~40M fuerza avg).
- Explicación: Objetos lisos (ej. Simbita: ~5-11M fuerza avg) producen menos cambios de intensidad locales y, por tanto, respuestas Harris más bajas.
🔬 Validación del Sistema Híbrido¶
Objetivo: Evitar conteos fijos (como 9,600 píxeles fijos al usar solo el percentil 98) y generar variabilidad real.
Resultado: ✅ ÉXITO
- El sistema híbrido (
min(estadístico, percentil)) logró su objetivo. - Las esquinas detectadas varían entre 1,440 y 1,900.
- Los bordes varían entre 2,400 y 9,700.
- Esto demuestra que las detecciones se adaptan al contenido real de cada imagen.
🎓 Conclusiones Finales¶
✅ Fortalezas Observadas¶
- Repetibilidad: Detecta las mismas esquinas bajo rotación y traslación.
- Velocidad: Muy eficiente y rápido computacionalmente.
- Precisión local: Localiza esquinas a nivel de píxel.
❌ Limitaciones Evidentes¶
- NO invariante a escala: Es la principal debilidad. El zoom cambia dramáticamente los resultados.
- Sensible a iluminación: Imágenes oscuras producen detecciones muy débiles.
- Dependiente de textura: Funciona mal en objetos lisos.
- Sin descriptor: Solo detecta, no describe (no sirve para matching por sí solo).
🎯 Recomendaciones de Uso¶
- ✅ Usar Harris para: Calibración de cámaras, tracking de video (escala fija), prototipado rápido o fines educativos.
- ❌ Evitar Harris para: Matching de objetos a diferentes distancias/escalas, o bajo iluminación muy variable.
🔄 Alternativas Modernas¶
| Característica | Harris | SIFT | ORB | Deep Learning |
|---|---|---|---|---|
| Invarianza rotación | ✅ | ✅ | ✅ | ✅ |
| Invarianza escala | ❌ | ✅ | ✅ | ✅ |
| Velocidad | ⚡⚡⚡ | ⚡ | ⚡⚡ | ⚡ (GPU) |
| Descriptor incluido | ❌ | ✅ | ✅ | ✅ |
| Calidad detección | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Reflexión Final: Nuestras Lecciones sobre el Detector de Harris¶
Nuestra experiencia con el detector de Harris fue una lección fundamental. Descubrimos que la visión computacional es mucho más que la implementación matemática; es una disciplina que exige comprender el comportamiento del algoritmo, depurar parámetros y aprender de los errores.
El Reto Principal: La Crisis de las 9,600 Esquinas¶
Nuestro momento más frustrante, y a la vez más educativo, fue cuando todas nuestras imágenes detectaban exactamente 9,600 esquinas, sin importar el contenido. Tras una intensa depuración, descubrimos que no se trataba de un bug en el código, sino de un error conceptual de matemáticas.
Nuestro sistema de umbral "híbrido" estaba fallando. El criterio del percentil (98º) siempre "ganaba" sobre el criterio estadístico. Dado que nuestras imágenes tenían 480,000 píxeles, el 2% superior siempre correspondía a 9,600 píxeles. Habíamos convertido nuestro detector en un simple selector del "top 2%".
Solución e Iteración¶
Comprendimos que debíamos cambiar de estrategia. Mediante un proceso iterativo de ajuste de parámetros (probando percentiles 99, 99.5, 99.8), logramos encontrar un balance (Sigma 0.5 + Percentil 99.7) donde el criterio estadístico podía competir con el percentil.
El resultado fue la obtención de variabilidad real (entre 1,440 y 1,900 esquinas), demostrando que el detector finalmente respondía al contenido de la imagen y no a una cantidad fija.
Descubrimientos Técnicos Clave¶
- Harris no es invariante a escala: Descubrimos que el zoom no solo agranda la imagen, sino que altera fundamentalmente la detección. Ver la fuerza de respuesta multiplicarse por 8 nos hizo apreciar por qué existen algoritmos como SIFT y ORB.
- La iluminación es crítica: Sufrimos una caída del 87% en la fuerza de detección entre una imagen normal y una oscura. Esto nos demostró que el preprocesamiento (como la ecualización de histograma) no es opcional.
- La calibración de parámetros es un arte: Encontrar el balance correcto entre detectar suficientes esquinas, no detectar ruido y mantener la variabilidad fue un proceso de prueba y error sistemático.
Principales Lecciones Aprendidas¶
- Comprensión Profunda: Pasamos de ver los percentiles como simples porcentajes a entenderlos como cantidades fijas para imágenes del mismo tamaño.
- Contexto Histórico: Leer el paper original de Harris & Stephens (1988) nos ayudó a entender el contexto revolucionario del algoritmo en su época, antes de las GPUs y el deep learning.
- Pensamiento Crítico: Aprendimos a cuestionar los "valores mágicos" de los tutoriales y a entender el porqué detrás de cada decisión de diseño.
- Depuración Sistemática: El "bug" de las 9,600 esquinas nos enseñó un proceso de depuración (aislar, hipotetizar, probar, validar) transferible a cualquier problema de ingeniería.
Conclusión¶
Concluimos que la visión computacional es tanto un arte como una ciencia. Los algoritmos clásicos como Harris siguen siendo relevantes por su velocidad y simplicidad, pero su uso efectivo requiere un profundo entendimiento de sus limitaciones.
Empezamos queriendo implementar una ecuación ($R = \det(M) - k \cdot \text{trace}(M)^2$), pero terminamos entendiendo cómo las máquinas "ven" el mundo. Esta lección, para nosotros, fue mucho más valiosa que cualquier número de esquinas detectadas.
📚 Referencias¶
Fuentes Primarias¶
Harris, C., & Stephens, M. (1988). A combined corner and edge detector. Proceedings of the Alvey Vision Conference, 147-151. https://doi.org/10.5244/C.2.23
Lowe, D. G. (2004). Distinctive image features from scale-invariant keypoints. International Journal of Computer Vision, 60(2), 91-110. https://doi.org/10.1023/B:VISI.0000029664.99615.94
Rosten, E., & Drummond, T. (2006). Machine learning for high-speed corner detection. European Conference on Computer Vision (pp. 430-443). Springer. https://doi.org/10.1007/11744023_34
Rublee, E., Rabaud, V., Konolige, K., & Bradski, G. (2011). ORB: An efficient alternative to SIFT or SURF. 2011 International Conference on Computer Vision (pp. 2564-2571). IEEE. https://doi.org/10.1109/ICCV.2011.6126544
Libros de Texto¶
Szeliski, R. (2022). Computer vision: Algorithms and applications (2nd ed.). Springer Nature. https://doi.org/10.1007/978-3-030-34372-9
Forsyth, D. A., & Ponce, J. (2012). Computer vision: A modern approach (2nd ed.). Pearson Education.
Hartley, R., & Zisserman, A. (2004). Multiple view geometry in computer vision (2nd ed.). Cambridge University Press. https://doi.org/10.1017/CBO9780511811685
Prince, S. J. D. (2012). Computer vision: Models, learning, and inference. Cambridge University Press. https://doi.org/10.1017/CBO9780511996504
Documentación Técnica¶
Bradski, G., & Kaehler, A. (2008). Learning OpenCV: Computer vision with the OpenCV library. O'Reilly Media.
OpenCV. (2024). Feature detection and description. OpenCV Documentation. https://docs.opencv.org/4.x/db/d27/tutorial_py_table_of_contents_feature2d.html
NumPy Developers. (2024). NumPy user guide. NumPy Documentation. https://numpy.org/doc/stable/user/index.html
SciPy Developers. (2024). SciPy reference guide. SciPy Documentation. https://docs.scipy.org/doc/scipy/reference/
Artículos Metodológicos¶
Mikolajczyk, K., & Schmid, C. (2005). A performance evaluation of local descriptors. IEEE Transactions on Pattern Analysis and Machine Intelligence, 27(10), 1615-1630. https://doi.org/10.1109/TPAMI.2005.188
Tuytelaars, T., & Mikolajczyk, K. (2008). Local invariant feature detectors: A survey. Foundations and Trends in Computer Graphics and Vision, 3(3), 177-280. https://doi.org/10.1561/0600000017
Deep Learning Approaches¶
DeTone, D., Malisiewicz, T., & Rabinovich, A. (2018). SuperPoint: Self-supervised interest point detection and description. Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition Workshops (pp. 224-236). https://doi.org/10.1109/CVPRW.2018.00060
Dusmanu, M., Rocco, I., Pajdla, T., Pollefeys, M., Sivic, J., Torii, A., & Sattler, T. (2019). D2-Net: A trainable CNN for joint description and detection of local features. Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 8092-8101). https://doi.org/10.1109/CVPR.2019.00828
Material Académico¶
Ochoa Ruiz, G. (2024). Módulo 3.2: Extracción de descriptores [Material de curso]. Tecnológico de Monterrey, Escuela de Ingeniería y Ciencias.
Recursos Web¶
Mathworks. (2024). Corner detection. MATLAB Documentation. https://www.mathworks.com/help/vision/ug/corner-detection.html
PyImageSearch. (2024). Corner detection. PyImageSearch Tutorials. https://pyimagesearch.com/
Software y Herramientas¶
Van Rossum, G., & Drake, F. L. (2009). Python 3 reference manual. CreateSpace.
Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90-95. https://doi.org/10.1109/MCSE.2007.55
McKinney, W. (2010). Data structures for statistical computing in Python. Proceedings of the 9th Python in Science Conference (pp. 56-61). https://doi.org/10.25080/Majora-92bf1922-00a